package chloroform

import _root_.util.logging.Logged
import java.lang.String
import argo.jdom._
import collection.immutable.List
import _root_.util.ColUtils._
import java.util.Map.Entry
import util.JsonRendererUtil
import java.io._
import java.util.concurrent.locks.ReentrantLock

/**
 * <p>
 * Created by IntelliJ IDEA.
 * User: zslajchrt
 * Date: Feb 20, 2011
 * Time: 6:23:17 PM
 */

@SerialVersionUID(1)
class JsonProjectLoader(resourceResolver: => ResourceResolver) extends ProjectLoader with Logged {

  protected def createJsonProject(projectName: String, projectResource: Resource, projectModel: JsonRootNode): JsonProject = {
    new JsonProject(projectName, projectResource, projectModel)
  }

  def loadProject(projectName: String) = {
    val projectResource = resourceResolver.resolve(projectName + ".json")
    val projectReader = new InputStreamReader(projectResource.openInputStream, "utf-8")

    val jdomParser = new JdomParser()
    val projectModel: JsonRootNode = jdomParser.parse(projectReader)

    debug("Loading project " + projectName + " definition: " + JsonRendererUtil.renderNodeToString(projectModel))

    projectReader.close

    createJsonProject(projectName, projectResource, projectModel)
  }

  @SerialVersionUID(1)
  class JsonProject(projectName: String, projectResource: Resource, projectModel: JsonRootNode) extends Project
  with JsonProjectSaver with StreamJsonSaver with ResourceWritable with ConsistencyVerifier with MutableProject {

    val destinationResource = projectResource
    val destinationEncoding = "utf-8"
    val getProjectName = projectModel.getStringValue("projectName")
    val writeLock: ReentrantLock = new ReentrantLock

    private[this] var entries: List[EntryType] = {
      val configsNodes: List[JsonStringNode] = projectModel.getObjectNode("configs").keySet
      configsNodes.map(entryNode => loadEntry(entryNode.getText))
    }

    def getProjectLoader() = JsonProjectLoader.this

    def getEntries = entries

    def getEntryNames = entries.map(_.name)

    def verifyConsistency(entry: EntryType): Option[ConsistencyViolation] = None

    trait InnerProjectEntry extends JsonProjectEntry {
      override def addExtension(extEntry: ProjectEntry) = {
        super.addExtension(adoptEntry(extEntry))
      }

      override def replaceExtension(extEntry: ProjectEntry) = {
        super.replaceExtension(adoptEntry(extEntry))
      }
    }

    def createProjectEntry(packagedArgs: ProjectEntry): ProjectEntry with InnerProjectEntry =
      new ProjectEntry(packagedArgs) with InnerProjectEntry

    private def loadEntry(pageName: String): ProjectEntry with InnerProjectEntry = {
      val rootNodeMap = projectModel.getObjectNode("configs", pageName);
      val rootNode = JsonNodeFactories.aJsonObject(rootNodeMap)
      loadEntry(pageName, rootNode)
    }

    private def loadEntry(pageName: String, rootNode: JsonNode): ProjectEntry with InnerProjectEntry = {
      try {
        val factory: String = rootNode.getStringValue("factory")
        val cookies = JsonProjectCookie(rootNode.getArrayNode("cookies")).
          map(new ProjectCookie(_) with JsonProjectCookie with ProjectCookieModificationAspect {
          val lock = writeLock
        })

        val scenarios: List[String] = if (rootNode.isArrayNode("scenarios")) {
          val scenariosAsNodes: List[JsonNode] = rootNode.getArrayNode("scenarios")
          scenariosAsNodes.map(_.getText)
        } else List("")

        val extensions: List[ProjectEntry with InnerProjectEntry] = if (rootNode.isObjectNode("extensions")) {
          val extNodes: java.util.Map[JsonStringNode, JsonNode] = rootNode.getObjectNode("extensions")
          val extEntryNodesIter = extNodes.entrySet.iterator

          def createExtensions(): List[ProjectEntry with InnerProjectEntry] = {
            if (!extEntryNodesIter.hasNext)
              Nil
            else {
              val extNodeEntry = extEntryNodesIter.next
              val extEntry = loadEntry(extNodeEntry.getKey.getStringValue(), extNodeEntry.getValue)
              // the tail recursion
              extEntry :: createExtensions
            }
          }
          createExtensions
        } else Nil

        val entry = createProjectEntry(new ProjectEntry(pageName, factory, true /*todo: read from the persistent entry*/ ,
          scenarios.toSet, cookies, extensions))
        extensions.foreach(_.extendedEntry = entry)
        entry
      }
      catch {
        case e: JsonNodeDoesNotMatchPathElementsException =>
          throw new IllegalArgumentException("Page '" + pageName + "' cannot be loaded from project " + projectName, e)
      }
    }

    def getEntry(entryName: String) = getEntries.find(_.name == entryName) match {
      case None => error(entryName + " entry not found")
      case Some(entry) => entry
    }

    def getScenarios = {
      var scenariosSet: Set[String] = Set.empty
      getEntries.foreach(entry => {
        entry.scenarios.foreach(scenariosSet += _)
      })
      scenariosSet
    }

    private def adoptEntry(entry: ProjectEntry) = {
      val newEntry: ProjectEntry with InnerProjectEntry = {
        val newCookies = entry.cookies.map(new ProjectCookie(_) with JsonProjectCookie with ProjectCookieModificationAspect {
            val lock = writeLock
          })
        createProjectEntry(new ProjectEntry(entry.name, entry.factory, entry.isPublic, entry.scenarios,
          newCookies, entry.extensions))
      }
      newEntry.setNew
      newEntry
    }

    def addEntry(entry: ProjectEntry, replaceAllowed: Boolean) = {
      this.synchronized {
        entries.find(_.name == entry.name) match {
          case Some(conflictEntry) => {
            if (!replaceAllowed)
              error("Project already contains " + entry.name + " entry")
            else {
              // replace
              removeEntry(entry.name)
              addEntry(entry, false)
            }
          }
          case None =>
            val newEntry = adoptEntry(entry)
            entries ::= newEntry
        }
      }
    }

    def removeEntry(entryName: String) = {
      this.synchronized {
        entries = entries.filter(entryName != _.name)
      }
    }

    def unlock() = {
      writeLock.unlock
    }

    def lock() = {
      writeLock.lock
    }
  }

}

trait ProjectCookieModificationAspect extends ProjectCookie {
  private[this] var dirty: Boolean = false

  val lock: ReentrantLock

  def isDirty = dirty

  def clearDirty = dirty = false

  override def setProperty(propertyName: String, valueNode: JsonNode) = {
    if (lock.isLocked) {
      error("Project is locked")
    }
    dirty = true
    super.setProperty(propertyName, valueNode)
  }
}

trait JsonProjectElement[T <: JsonNode] {
  def createBuilder(): JsonNodeBuilder[T]
}

trait JsonSaver {
  def save(rootBuilder: JsonObjectNodeBuilder)
}

trait Writable {
  def getWriter(): Writer
}

trait ResourceWritable extends Writable {
  val destinationResource: Resource
  val destinationEncoding: String

  def getWriter() = {
    try {
      val outputStream: OutputStream = destinationResource.openOutputStream
      new OutputStreamWriter(outputStream, destinationEncoding)
    }
    catch {
      case t: Throwable => throw new UnsupportedOperationException("Cannot open output stream for resource " +
        destinationResource + ": " + t.getMessage, t)
    }
  }
}

trait StreamJsonSaver extends JsonSaver with Writable {
  def save(rootBuilder: JsonObjectNodeBuilder) = {
    val renderer = JsonRendererUtil.makeRenderer(rootBuilder)
    val writer = getWriter()
    try {
      renderer.render(writer)
    }
    finally {
      writer.close
    }
  }
}

trait JsonProjectSaver extends ProjectSaver with JsonProjectElement[JsonRootNode] with JsonSaver {

  type EntryType = ProjectEntry with JsonProjectEntry with JsonProjectElement[_ <: JsonNode]

  def getEntry(entryName: String): EntryType

  def getEntries(): List[EntryType]

  def save() = {
    val projectJsonBuilder = createBuilder
    save(projectJsonBuilder)
  }

  def createBuilder() = {
    val rootBuilder = JsonNodeBuilders.anObjectBuilder
    rootBuilder.withField("projectName", JsonNodeBuilders.aStringBuilder(getProjectName))

    val configsBuilder = JsonNodeBuilders.anObjectBuilder
    rootBuilder.withField("configs", configsBuilder)

    getEntries.foreach(entry => {
      val entryBuilder: JsonNodeBuilder[_ <: JsonNode] = entry.createBuilder
      configsBuilder.withField(entry.name, entryBuilder)
      entry.clearDirty
    })

    rootBuilder
  }
}

object ConsistencyViolation {
  def apply(description: String, throwable: Throwable) = new ConsistencyViolation(description, throwable)

  def apply(throwable: Throwable) = {

    def getDescription(throwable: Throwable): String = {
      throwable.getMessage match {
        case null => throwable.getCause match {
          case null => throwable.toString()
          case cause: Throwable => getDescription(cause)
        }
        case msg: String => msg
      }
    }

    new ConsistencyViolation(getDescription(throwable), throwable)
  }
}

@serializable
@SerialVersionUID(1L)
class ConsistencyViolation(val description: String, val throwable: Throwable)

class ConsistencyViolationException(val violations: Map[String, ConsistencyViolation]) extends RuntimeException {
  override def getMessage = {
    violations.foldLeft("Consistency violations:") {
      (acc, pair) => acc + "\n" + pair._2.description
    }
  }
}

trait ConsistencyVerifier extends JsonProjectSaver {

  override def save() = {
    var violations = Map.empty[String, ConsistencyViolation]
    getEntries.filter(_.isDirty).foreach(entry => verifyConsistency(entry) match {
      case None =>
      case Some(violation) => {
        violations += (entry.name -> violation)
      }
    })

    if (!violations.isEmpty)
      throw new ConsistencyViolationException(violations)

    super.save
  }

  def verifyConsistency(entry: EntryType): Option[ConsistencyViolation]
}

trait DummyConsistencyVerifier extends ConsistencyVerifier {
  def verifyConsistency(entry: EntryType): Option[ConsistencyViolation] = None
}

object JsonProjectCookie {
  def apply(cookieNodesAsList: java.util.List[JsonNode]): List[ProjectCookie] = {
    val cookieNodes: List[JsonNode] = cookieNodesAsList
    cookieNodes.map(cookieNode => new ProjectCookie(cookieNode.getStringValue("pageId"),
      cookieNode.getObjectNode("properties")))
  }
}

trait JsonProjectCookie extends ProjectCookie with JsonProjectElement[JsonRootNode] {

  def createBuilder() = {
    val cookieBuilder = JsonNodeBuilders.anObjectBuilder
    cookieBuilder.withField("pageId", JsonNodeBuilders.aStringBuilder(this.pageId))

    val propertiesBuilder = JsonNodeBuilders.anObjectBuilder
    cookieBuilder.withField("properties", propertiesBuilder)

    val pairs: java.util.Iterator[Entry[JsonStringNode, JsonNode]] = properties.entrySet.iterator
    while (pairs.hasNext) {
      val pair = pairs.next
      propertiesBuilder.withField(pair.getKey.getText, new JsonNodeBuilder[JsonNode] {
        def build = pair.getValue
      })
    }

    cookieBuilder
  }
}

trait JsonProjectEntry extends ProjectEntry with JsonProjectElement[JsonRootNode] {

  private var isNew: Boolean = false

  def setNew() {
    isNew = true
  }

  def isDirty: Boolean = isNew || cookies.map(_.asInstanceOf[ProjectCookieModificationAspect]).find(_.isDirty) != None

  def clearDirty() {
    cookies.map(_.asInstanceOf[ProjectCookieModificationAspect]).foreach(_.clearDirty)
    isNew = false
  }

  def createBuilder(): JsonNodeBuilder[JsonRootNode] = {
    val entryBuilder = JsonNodeBuilders.anObjectBuilder
    entryBuilder.withField("factory", JsonNodeBuilders.aStringBuilder(factory))

    val scenariosBuilder = JsonNodeBuilders.anArrayBuilder
    entryBuilder.withField("scenarios", scenariosBuilder)
    scenarios.foreach(scenario => {
      scenariosBuilder.withElement(JsonNodeBuilders.aStringBuilder(scenario))
    })

    val cookiesBuilder = JsonNodeBuilders.anArrayBuilder
    entryBuilder.withField("cookies", cookiesBuilder)
    cookies.foreach(cookie => {
      val jsonCookie = cookie.asInstanceOf[JsonProjectCookie]
      cookiesBuilder.withElement(jsonCookie.createBuilder)
    })

    val extensionsBuilder = JsonNodeBuilders.anObjectBuilder
    entryBuilder.withField("extensions", extensionsBuilder)
    extensions.foreach(extension => {
      val jsonExtEntry: JsonProjectEntry = extension.asInstanceOf[JsonProjectEntry]
      val extBuilder: JsonNodeBuilder[_ <: JsonNode] = jsonExtEntry.createBuilder
      extensionsBuilder.withField(extension.name, extBuilder)
      jsonExtEntry.clearDirty
    })

    entryBuilder
  }
}